<template>
  <div class="zhejiao-container">
    <el-card class="upload-card">
      <template #header>
        <div class="card-header">
          <h2>折角检测</h2>
        </div>
      </template>
      
      <div class="upload-area">
        <el-upload
          class="upload-component"
          drag
          multiple
          accept="image/*"
          :auto-upload="false"
          :on-change="handleFileChange"
          :file-list="fileList"
        >
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div class="el-upload__text">
            将文件拖到此处，或<em>点击上传</em>
          </div>
          <template #tip>
            <div class="el-upload__tip">
              请上传A4扫描件图片
            </div>
          </template>
        </el-upload>
      </div>

      <div class="preview-area" v-if="previewImages.length > 0">
        <div v-for="(image, index) in previewImages" :key="index" class="preview-item">
          <div class="image-container" :class="{ 'has-zhejiao': image.hasZhejiao }">
            <canvas :ref="(el) => { if(el && 'getContext' in el) imageCanvasRefs[index] = el as HTMLCanvasElement }" class="edge-canvas"></canvas>
            <img :src="image.url" :alt="'预览图 ' + (index + 1)">
            <div v-if="image.hasZhejiao" class="fold-warning">
              <el-icon><warning-filled /></el-icon>
              <span>有折角</span>
            </div>
          </div>
          <el-button 
            v-if="!image.hasZhejiao"
            type="primary" 
            @click="handleUpload(image)"
            :loading="image.uploading"
          >
            上传
          </el-button>
        </div>
      </div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { UploadFilled, WarningFilled } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

interface PreviewImage {
  url: string
  file: File
  hasZhejiao: boolean
  uploading: boolean
  edges?: { x: number; y: number }[] // 使用对象类型来存储坐标点
  foldDetected: boolean
}

const fileList = ref<any[]>([])
const previewImages = ref<PreviewImage[]>([])
const imageCanvasRefs = ref<{ [key: number]: HTMLCanvasElement | null }>({})

// DBSCAN聚类实现（用于异常点聚类）
const dbscan = (points: { x: number; y: number }[], eps: number, minPts: number) => {
  const labels = new Array(points.length).fill(-1)
  let clusterId = 0
  const dist = (a: { x: number; y: number }, b: { x: number; y: number }) => Math.hypot(a.x - b.x, a.y - b.y)
  for (let i = 0; i < points.length; i++) {
    if (labels[i] !== -1) continue
    // 找邻域
    const neighbors = []
    for (let j = 0; j < points.length; j++) {
      if (dist(points[i], points[j]) <= eps) neighbors.push(j)
    }
    if (neighbors.length < minPts) {
      labels[i] = -2 // 噪声
      continue
    }
    // 新簇
    labels[i] = clusterId
    let k = 0
    while (k < neighbors.length) {
      const n = neighbors[k]
      if (labels[n] === -2) labels[n] = clusterId
      if (labels[n] !== -1) { k++; continue }
      labels[n] = clusterId
      // 扩展邻域
      const nNeighbors = []
      for (let j = 0; j < points.length; j++) {
        if (dist(points[n], points[j]) <= eps) nNeighbors.push(j)
      }
      if (nNeighbors.length >= minPts) neighbors.push(...nNeighbors)
      k++
    }
    clusterId++
  }
  return labels
}

// 检测折角的函数（智能聚类+面积/长度占比+分布分析，参数更灵敏）
const detectZhejiao = async (file: File): Promise<{
  hasZhejiao: boolean;
  mainEdges: { x: number; y: number }[][];
  foldEdges: { x: number; y: number }[][];
  foldTips: { x: number; y: number }[];
}> => {
  return new Promise((resolve) => {
    const img = new Image()
    img.onload = () => {
      try {
        // 创建canvas并绘制图片
        const canvas = document.createElement('canvas')
        const ctx = canvas.getContext('2d')
        if (!ctx) {
          resolve({ hasZhejiao: false, mainEdges: [], foldEdges: [], foldTips: [] })
          return
        }
        canvas.width = img.width
        canvas.height = img.height
        ctx.drawImage(img, 0, 0)
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
        const data = imageData.data
        // 1. 灰度化
        const grayData = grayscale(data, canvas.width, canvas.height)
        // 2. 高斯模糊
        const blurredData = gaussianBlur(grayData, canvas.width, canvas.height)
        // 3. Canny边缘检测
        const edges = cannyEdgeDetection(blurredData, canvas.width, canvas.height)
        // 4. 轮廓提取，选最大轮廓
        const contours = findContours(edges, canvas.width, canvas.height)
        if (contours.length === 0) {
          resolve({ hasZhejiao: false, mainEdges: [], foldEdges: [], foldTips: [] })
          return
        }
        const maxContour = contours.reduce((a, b) => (a.length > b.length ? a : b))
        // 5. 多边形逼近
        const approx = approxPolyDP(maxContour, 0.02 * Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height))
        // 6. 最小外接四边形
        const minRect = minAreaRect(maxContour)
        // 7. 校验重合度
        const overlap = polygonOverlapRatio(approx, minRect)
        // 8. 主边
        const mainEdges = [] as { x: number; y: number }[][]
        for (let i = 0; i < 4; i++) {
          mainEdges.push([
            minRect[i],
            minRect[(i + 1) % 4]
          ])
        }
        // 9. 异常区域分析（智能聚类，参数更灵敏）
        let foldEdges: { x: number; y: number }[][] = []
        let foldTips: { x: number; y: number }[] = []
        let hasZhejiao = false
        if (approx.length === 4 && overlap > 0.92) {
          hasZhejiao = false
        } else {
          // 计算异常点
          const deviation = findContourDeviations(maxContour, minRect, 15)
          // DBSCAN聚类（更灵敏）
          const eps = Math.max(canvas.width, canvas.height) * 0.018
          const minPts = 3
          const labels = dbscan(deviation.abnormalPoints, eps, minPts)
          // 聚类结果转为边
          const clusters: { x: number; y: number }[][] = []
          const tips: { x: number; y: number }[] = []
          for (let cid = 0; cid < Math.max(...labels) + 1; cid++) {
            const cluster = deviation.abnormalPoints.filter((_, idx) => labels[idx] === cid)
            if (cluster.length > 1) {
              clusters.push([cluster[0], cluster[cluster.length - 1]])
              tips.push(cluster[0])
            }
          }
          // 计算异常区域总长度
          let abnormalLen = 0
          clusters.forEach(edge => {
            abnormalLen += Math.sqrt((edge[0].x - edge[1].x) ** 2 + (edge[0].y - edge[1].y) ** 2)
          })
          // 计算A4主边总长度
          let mainLen = 0
          for (let i = 0; i < 4; i++) {
            mainLen += Math.sqrt((minRect[i].x - minRect[(i + 1) % 4].x) ** 2 + (minRect[i].y - minRect[(i + 1) % 4].y) ** 2)
          }
          // 只要有一簇异常点长度占主边长度比例大于0.03（3%），即判定为折角
          if (clusters.some(edge => Math.sqrt((edge[0].x - edge[1].x) ** 2 + (edge[0].y - edge[1].y) ** 2) / mainLen > 0.03)) {
            hasZhejiao = true
            foldEdges = clusters
            foldTips = tips
          } else {
            hasZhejiao = false
          }
        }
        resolve({ hasZhejiao, mainEdges, foldEdges, foldTips })
      } catch (error) {
        console.error('检测过程出错:', error)
        resolve({ hasZhejiao: false, mainEdges: [], foldEdges: [], foldTips: [] })
      }
    }
    img.src = URL.createObjectURL(file)
  })
}

// 多边形重合度（交集面积/并集面积，简化为顶点距离均值）
const polygonOverlapRatio = (a: { x: number; y: number }[], b: { x: number; y: number }[]) => {
  if (a.length !== 4 || b.length !== 4) return 0
  let sumDist = 0
  for (let i = 0; i < 4; i++) {
    sumDist += Math.sqrt((a[i].x - b[i].x) ** 2 + (a[i].y - b[i].y) ** 2)
  }
  const diag = Math.sqrt((b[0].x - b[2].x) ** 2 + (b[0].y - b[2].y) ** 2)
  return 1 - sumDist / (4 * diag)
}

// 最小外接四边形拟合（简单凸包+旋转卡壳）
const minAreaRect = (points: { x: number; y: number }[]): { x: number; y: number }[] => {
  // 1. 计算凸包
  const hull = convexHull(points)
  if (hull.length <= 4) return hull
  // 2. 穷举所有边，找最小面积外接矩形
  let minArea = Infinity
  let bestRect: { x: number; y: number }[] = []
  for (let i = 0; i < hull.length; i++) {
    const p1 = hull[i]
    const p2 = hull[(i + 1) % hull.length]
    const angle = Math.atan2(p2.y - p1.y, p2.x - p1.x)
    const cos = Math.cos(-angle)
    const sin = Math.sin(-angle)
    // 旋转所有点
    const rot = hull.map(pt => ({
      x: pt.x * cos - pt.y * sin,
      y: pt.x * sin + pt.y * cos
    }))
    const xs = rot.map(p => p.x)
    const ys = rot.map(p => p.y)
    const minX = Math.min(...xs), maxX = Math.max(...xs)
    const minY = Math.min(...ys), maxY = Math.max(...ys)
    const area = (maxX - minX) * (maxY - minY)
    if (area < minArea) {
      minArea = area
      // 逆旋转回去
      bestRect = [
        { x: minX, y: minY },
        { x: maxX, y: minY },
        { x: maxX, y: maxY },
        { x: minX, y: maxY }
      ].map(pt => ({
        x: pt.x * cos + pt.y * sin,
        y: -pt.x * sin + pt.y * cos
      }))
    }
  }
  return bestRect
}
// 凸包（Graham扫描）
const convexHull = (points: { x: number; y: number }[]): { x: number; y: number }[] => {
  const sorted = points.slice().sort((a, b) => a.x - b.x || a.y - b.y)
  const cross = (o: any, a: any, b: any) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
  const lower: { x: number; y: number }[] = []
  for (const p of sorted) {
    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop()
    lower.push(p)
  }
  const upper: { x: number; y: number }[] = []
  for (let i = sorted.length - 1; i >= 0; i--) {
    const p = sorted[i]
    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop()
    upper.push(p)
  }
  upper.pop(); lower.pop()
  return lower.concat(upper)
}
// findContourDeviations返回异常点数组
const findContourDeviations = (
  contour: { x: number; y: number }[],
  rect: { x: number; y: number }[],
  threshold: number
): { foldEdges: { x: number; y: number }[][]; foldTips: { x: number; y: number }[]; abnormalPoints: { x: number; y: number }[] } => {
  const pointToLineDist = (p: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }) => {
    const A = p.x - a.x
    const B = p.y - a.y
    const C = b.x - a.x
    const D = b.y - a.y
    const dot = A * C + B * D
    const len_sq = C * C + D * D
    let param = -1
    if (len_sq !== 0) param = dot / len_sq
    let xx, yy
    if (param < 0) {
      xx = a.x; yy = a.y
    } else if (param > 1) {
      xx = b.x; yy = b.y
    } else {
      xx = a.x + param * C
      yy = a.y + param * D
    }
    const dx = p.x - xx
    const dy = p.y - yy
    return Math.sqrt(dx * dx + dy * dy)
  }
  const abnormalPoints: { x: number; y: number }[] = []
  for (const pt of contour) {
    let minDist = Infinity
    for (let i = 0; i < 4; i++) {
      const a = rect[i], b = rect[(i + 1) % 4]
      const dist = pointToLineDist(pt, a, b)
      if (dist < minDist) minDist = dist
    }
    if (minDist > threshold) abnormalPoints.push(pt)
  }
  // 兼容老接口
  return { foldEdges: [], foldTips: [], abnormalPoints }
}

// 轮廓提取（简单实现，8邻域跟踪）
const findContours = (edges: Uint8Array, width: number, height: number): { x: number; y: number }[][] => {
  const visited = new Uint8Array(width * height)
  const contours: { x: number; y: number }[][] = []
  const directions = [
    [-1, -1], [-1, 0], [-1, 1],
    [0, -1],          [0, 1],
    [1, -1],  [1, 0], [1, 1]
  ]
  for (let i = 1; i < height - 1; i++) {
    for (let j = 1; j < width - 1; j++) {
      const idx = i * width + j
      if (edges[idx] === 255 && !visited[idx]) {
        // 新轮廓
        const contour: { x: number; y: number }[] = []
        const stack = [{ x: j, y: i }]
        while (stack.length > 0) {
          const { x, y } = stack.pop()!
          const cidx = y * width + x
          if (x < 0 || x >= width || y < 0 || y >= height) continue
          if (visited[cidx]) continue
          if (edges[cidx] !== 255) continue
          visited[cidx] = 1
          contour.push({ x, y })
          for (const [di, dj] of directions) {
            stack.push({ x: x + dj, y: y + di })
          }
        }
        if (contour.length > 100) contours.push(contour)
      }
    }
  }
  return contours
}

// 灰度化处理
const grayscale = (data: Uint8ClampedArray, width: number, height: number): Uint8Array => {
  const grayData = new Uint8Array(width * height)
  for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
      const idx = (i * width + j) * 4
      // 使用加权平均法进行灰度化
      grayData[i * width + j] = Math.round(
        data[idx] * 0.299 + data[idx + 1] * 0.587 + data[idx + 2] * 0.114
      )
    }
  }
  return grayData
}

// 高斯模糊
const gaussianBlur = (data: Uint8Array, width: number, height: number): Uint8Array => {
  const kernel = [
    [1, 2, 1],
    [2, 4, 2],
    [1, 2, 1]
  ]
  const kernelSum = 16
  const result = new Uint8Array(width * height)

  for (let i = 1; i < height - 1; i++) {
    for (let j = 1; j < width - 1; j++) {
      let sum = 0
      for (let k = -1; k <= 1; k++) {
        for (let l = -1; l <= 1; l++) {
          const idx = (i + k) * width + (j + l)
          sum += data[idx] * kernel[k + 1][l + 1]
        }
      }
      result[i * width + j] = Math.round(sum / kernelSum)
    }
  }

  return result
}

// Canny边缘检测
const cannyEdgeDetection = (data: Uint8Array, width: number, height: number): Uint8Array => {
  // 1. 使用Sobel算子计算梯度
  const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
  const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1]
  const gradientX = new Int16Array(width * height)
  const gradientY = new Int16Array(width * height)
  const magnitude = new Uint8Array(width * height)
  const direction = new Float32Array(width * height)

  for (let i = 1; i < height - 1; i++) {
    for (let j = 1; j < width - 1; j++) {
      let gx = 0
      let gy = 0
      for (let k = -1; k <= 1; k++) {
        for (let l = -1; l <= 1; l++) {
          const idx = (i + k) * width + (j + l)
          const weight = sobelX[(k + 1) * 3 + (l + 1)]
          gx += data[idx] * weight
          gy += data[idx] * sobelY[(k + 1) * 3 + (l + 1)]
        }
      }
      const idx = i * width + j
      gradientX[idx] = gx
      gradientY[idx] = gy
      magnitude[idx] = Math.sqrt(gx * gx + gy * gy)
      direction[idx] = Math.atan2(gy, gx)
    }
  }

  // 2. 非极大值抑制
  const suppressed = new Uint8Array(width * height)
  for (let i = 1; i < height - 1; i++) {
    for (let j = 1; j < width - 1; j++) {
      const idx = i * width + j
      const angle = direction[idx]
      const mag = magnitude[idx]
      
      // 将角度转换为0-180度
      let angleDeg = (angle * 180 / Math.PI + 180) % 180
      
      // 确定梯度方向
      let r1, r2
      if ((angleDeg >= 0 && angleDeg < 22.5) || (angleDeg >= 157.5 && angleDeg < 180)) {
        r1 = magnitude[idx + 1]
        r2 = magnitude[idx - 1]
      } else if (angleDeg >= 22.5 && angleDeg < 67.5) {
        r1 = magnitude[idx + width + 1]
        r2 = magnitude[idx - width - 1]
      } else if (angleDeg >= 67.5 && angleDeg < 112.5) {
        r1 = magnitude[idx + width]
        r2 = magnitude[idx - width]
      } else {
        r1 = magnitude[idx + width - 1]
        r2 = magnitude[idx - width + 1]
      }

      suppressed[idx] = (mag >= r1 && mag >= r2) ? mag : 0
    }
  }

  // 3. 双阈值处理和滞后边缘跟踪
  const highThreshold = 50
  const lowThreshold = 20
  const edges = new Uint8Array(width * height)
  const visited = new Set<number>()

  // 递归函数用于滞后边缘跟踪
  const traceEdge = (idx: number) => {
    if (visited.has(idx)) return
    visited.add(idx)
    edges[idx] = 255

    const i = Math.floor(idx / width)
    const j = idx % width

    // 检查8个相邻像素
    for (let di = -1; di <= 1; di++) {
      for (let dj = -1; dj <= 1; dj++) {
        if (di === 0 && dj === 0) continue
        const ni = i + di
        const nj = j + dj
        if (ni >= 0 && ni < height && nj >= 0 && nj < width) {
          const nidx = ni * width + nj
          if (suppressed[nidx] >= lowThreshold) {
            traceEdge(nidx)
          }
        }
      }
    }
  }

  // 从强边缘点开始跟踪
  for (let i = 0; i < height; i++) {
    for (let j = 0; j < width; j++) {
      const idx = i * width + j
      if (suppressed[idx] >= highThreshold) {
        traceEdge(idx)
      }
    }
  }

  return edges
}

// 绘制边缘，主边蓝色，折角短边红色，折角顶点高亮
const drawEdges = (canvas: HTMLCanvasElement, mainEdges: { x: number; y: number }[][], foldEdges: { x: number; y: number }[][], foldTips: { x: number; y: number }[], width: number, height: number) => {
  const ctx = canvas.getContext('2d')
  if (!ctx) return
  canvas.width = width
  canvas.height = height
  // 主边蓝色
  ctx.strokeStyle = '#007aff'
  ctx.lineWidth = 3
  mainEdges.forEach(edge => {
    ctx.beginPath()
    ctx.moveTo(edge[0].x, edge[0].y)
    ctx.lineTo(edge[1].x, edge[1].y)
    ctx.stroke()
  })
  // 折角短边红色
  ctx.strokeStyle = '#ff0000'
  ctx.lineWidth = 3
  foldEdges.forEach(edge => {
    ctx.beginPath()
    ctx.moveTo(edge[0].x, edge[0].y)
    ctx.lineTo(edge[1].x, edge[1].y)
    ctx.stroke()
  })
  // 折角顶点高亮
  ctx.fillStyle = '#ff0000'
  foldTips.forEach(pt => {
    ctx.beginPath()
    ctx.arc(pt.x, pt.y, 10, 0, 2 * Math.PI)
    ctx.fill()
  })
}

// 处理文件选择
const handleFileChange = async (file: any) => {
  if (!file) return
  const isImage = file.raw.type.startsWith('image/')
  if (!isImage) {
    ElMessage.error('请上传图片文件！')
    return
  }
  const reader = new FileReader()
  reader.onload = async (e) => {
    try {
      const { hasZhejiao, mainEdges, foldEdges, foldTips } = await detectZhejiao(file.raw)
      const index = previewImages.value.length
      previewImages.value.push({
        url: e.target?.result as string,
        file: file.raw,
        hasZhejiao,
        uploading: false,
        edges: [],
        foldDetected: hasZhejiao
      })
      setTimeout(() => {
        const canvas = imageCanvasRefs.value[index]
        if (canvas) {
          const img = new Image()
          img.onload = () => {
            drawEdges(canvas, mainEdges, foldEdges, foldTips, img.width, img.height)
          }
          img.src = e.target?.result as string
        }
      }, 0)
    } catch (error) {
      ElMessage.error('图片处理失败，请重试！')
    }
  }
  reader.readAsDataURL(file.raw)
}

// 处理上传
const handleUpload = async (image: PreviewImage) => {
  image.uploading = true
  try {
    // 模拟上传过程
    await new Promise(resolve => setTimeout(resolve, 2000))
    ElMessage.success('上传成功！')
  } catch (error) {
    ElMessage.error('上传失败，请重试！')
  } finally {
    image.uploading = false
  }
}

// 多边形逼近（Ramer–Douglas–Peucker算法，简化实现）
const approxPolyDP = (points: { x: number; y: number }[], epsilon: number): { x: number; y: number }[] => {
  if (points.length < 3) return points
  const dmax = { dist: 0, idx: 0 }
  const lineDist = (p: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }) => {
    const num = Math.abs((b.y - a.y) * p.x - (b.x - a.x) * p.y + b.x * a.y - b.y * a.x)
    const den = Math.sqrt((b.y - a.y) ** 2 + (b.x - a.x) ** 2)
    return den === 0 ? 0 : num / den
  }
  for (let i = 1; i < points.length - 1; i++) {
    const d = lineDist(points[i], points[0], points[points.length - 1])
    if (d > dmax.dist) {
      dmax.dist = d
      dmax.idx = i
    }
  }
  if (dmax.dist > epsilon) {
    const rec1 = approxPolyDP(points.slice(0, dmax.idx + 1), epsilon)
    const rec2 = approxPolyDP(points.slice(dmax.idx), epsilon)
    return rec1.slice(0, -1).concat(rec2)
  } else {
    return [points[0], points[points.length - 1]]
  }
}
</script>

<style lang="scss" scoped>
.zhejiao-container {
  padding: 20px;
  
  .upload-card {
    max-width: 1200px;
    margin: 0 auto;
    
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      
      h2 {
        margin: 0;
        color: #409EFF;
      }
    }
    
    .upload-area {
      margin: 20px 0;
      
      .upload-component {
        width: 100%;
      }
    }
    
    .preview-area {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
      gap: 20px;
      margin-top: 20px;
      
      .preview-item {
        .image-container {
          position: relative;
          border: 2px solid #dcdfe6;
          border-radius: 4px;
          overflow: hidden;
          margin-bottom: 10px;
          
          &.has-zhejiao {
            border-color: #f56c6c;
          }
          
          img {
            width: 100%;
            height: auto;
            display: block;
          }

          .edge-canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
          }
          
          .fold-warning {
            position: absolute;
            top: 10px;
            left: 10px;
            background-color: rgba(245, 108, 108, 0.95);
            color: #fff;
            padding: 6px 16px;
            border-radius: 6px;
            font-weight: bold;
            font-size: 18px;
            z-index: 10;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
            display: flex;
            align-items: center;
            gap: 8px;
          }
        }
        
        .el-button {
          width: 100%;
        }
      }
    }
  }
}
</style> 